ARMベースの「Graviton2」インスタンスでDockerをいろいろ動かしてみた
みなさん、こんにちは!
AWS事業本部の青柳@福岡オフィスです。
re:Invent 2019 で発表された、第2世代のARMベースプロセッサ「Graviton2」を搭載した「M6gインスタンス」が、先日GAとなりました。
この新しいインスタンスタイプを使って何かやってみよう! ということで、Graviton2インスタンスで Docker を使っていろいろと試してみます。
その1: M6gインスタンスを起動してDockerを動かしてみる
インスタンス起動
AMIの選択で「Amazon Linux 2」を選択して、アーキテクチャは「64ビット (Arm)」を選択します。
インスタンスタイプの選択で、第1世代のARMベースインスタンス「A1」に加えて、第2世代のARMベースインスタンス「M6g」が選択できるようになっています。
あとは、適宜設定していってインスタンスを起動してください。
ポイントは以下の通りです。
- Dockerイメージを保存するために、ディスク容量は若干増やしておいた方が良いかもです。(例:8GB→20GB)
- 動作テストを行うために、セキュリティグループのインバウンドルールで「マイIP」からの「TCP/80」および「TCP/8080」の接続を許可してください。
CloudFormationで環境構築する場合は、以下のテンプレートを利用してください。
CloudFormationテンプレート (クリックすると展開します)
--- AWSTemplateFormatVersion: "2010-09-09" Description: "Launch 'Graviton2' EC2 instance with VPC environment" Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "General Information" Parameters: - SystemName - Label: default: "Network Configuration" Parameters: - CidrBlockVPC - CidrBlockSubnetPublic - MyIpAddressCidr - Label: default: "EC2 Instance Configuration" Parameters: - Graviton2ImageID - Graviton2InstanceType - Graviton2KeyName - Graviton2VolumeType - Graviton2VolumeSize Parameters: SystemName: Type: String Default: graviton2 CidrBlockVPC: Type: String Default: 192.168.0.0/16 CidrBlockSubnetPublic: Type: String Default: 192.168.1.0/24 MyIpAddressCidr: Type: String Graviton2ImageID: Type: AWS::SSM::Parameter::Value<String> Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2 Graviton2InstanceType: Type: String Default: m6g.medium AllowedValues: - m6g.medium - m6g.large - m6g.xlarge - m6g.2xlarge - m6g.4xlarge - m6g.8xlarge - m6g.12xlarge - m6g.16xlarge Graviton2KeyName: Type: AWS::EC2::KeyPair::KeyName Graviton2VolumeType: Type: String Default: gp2 Graviton2VolumeSize: Type: String Default: 20 Resources: VPC: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref CidrBlockVPC EnableDnsSupport: true EnableDnsHostnames: true InstanceTenancy: default Tags: - Key: Name Value: !Sub "${SystemName}-vpc" - Key: System Value: !Ref SystemName InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Sub "${SystemName}-igw" - Key: System Value: !Ref SystemName VPCGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: !Ref InternetGateway VpcId: !Ref VPC SubnetPublic: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select - 0 - Fn::GetAZs: !Ref AWS::Region CidrBlock: !Ref CidrBlockSubnetPublic MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Sub "${SystemName}-public-subnet" - Key: System Value: !Ref SystemName RouteTablePublic: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub "${SystemName}-public-rtb" - Key: System Value: !Ref SystemName RouteIGW: DependsOn: - VPCGatewayAttachment Type: AWS::EC2::Route Properties: RouteTableId: !Ref RouteTablePublic DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway RouteTableAssociationPublic: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref SubnetPublic RouteTableId: !Ref RouteTablePublic SecurityGroupServer: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub "${SystemName}-server-sg" GroupDescription: "Security group for server" VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: !Ref MyIpAddressCidr - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: !Ref MyIpAddressCidr - IpProtocol: tcp FromPort: 8080 ToPort: 8080 CidrIp: !Ref MyIpAddressCidr Tags: - Key: Name Value: !Sub "${SystemName}-server-sg" - Key: System Value: !Ref SystemName EC2InstanceGraviton2: Type: AWS::EC2::Instance Properties: ImageId: !Ref Graviton2ImageID InstanceType: !Ref Graviton2InstanceType KeyName: !Ref Graviton2KeyName BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: VolumeType: !Ref Graviton2VolumeType VolumeSize: !Ref Graviton2VolumeSize NetworkInterfaces: - DeviceIndex: 0 SubnetId: !Ref SubnetPublic GroupSet: - !Ref SecurityGroupServer Tags: - Key: Name Value: !Sub "${SystemName}-server" - Key: System Value: !Ref SystemName Outputs: VPC: Value: !Ref VPC Export: Name: !Sub "${AWS::StackName}::VPC" SubnetPublic: Value: !Ref SubnetPublic Export: Name: !Sub "${AWS::StackName}::SubnetPublic" SecurityGroupServer: Value: !Ref SecurityGroupServer Export: Name: !Sub "${AWS::StackName}::SecurityGroupServer" EC2InstanceGraviton2: Value: !Ref EC2InstanceGraviton2 Export: Name: !Sub "${AWS::StackName}::EC2InstanceGraviton2"
Dockerのインストール
インスタンスが起動しましたら、SSHで接続します。
「Amazon Linux Extras」を使ってDockerをインストールします。
$ sudo amazon-linux-extras install docker=latest
サービス自動起動の設定を行います。
$ sudo systemctl enable docker.service $ sudo systemctl start docker.service
「ec2-user」ユーザーを「docker」グループに所属させます。
(docekr
コマンドをsudo
を付けずに実行できるようにするための設定です)
$ sudo usermod -aG docker ec2-user
一度ログアウトしてから、再度ログインします。
docker
コマンドが実行できることを確認しましょう。
$ docker version Client: Version: 19.03.6-ce API version: 1.40 Go version: go1.13.4 Git commit: 369ce74 Built: Fri Apr 24 18:30:35 2020 OS/Arch: linux/arm64 Experimental: false Server: Engine: Version: 19.03.6-ce API version: 1.40 (minimum version 1.12) Go version: go1.13.4 Git commit: 369ce74 Built: Fri Apr 24 18:31:36 2020 OS/Arch: linux/arm64 Experimental: false containerd: Version: 1.3.2 GitCommit: ff48f57fc83a8c44cf4ad5d672424a98ba37ded6 runc: Version: 1.0.0-rc10 GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dd docker-init: Version: 0.18.0 GitCommit: fec3683
Dockerコンテナの実行
試しに「nginx」の公式コンテナイメージを使ってコンテナを起動してみます。
$ docker container run --name nginx -p 80:80 --rm nginx:latest Unable to find image 'nginx:latest' locally latest: Pulling from library/nginx b24dc5b5f4f0: Pull complete 8a4ea8d1205e: Pull complete 772e9fa36a03: Pull complete Digest: sha256:404ed8de56dd47adadadf9e2641b1ba6ad5ce69abf251421f91d7601a2808ebe Status: Downloaded newer image for nginx:latest
TCP/80ポートで待ち受け状態になります。
WebブラウザでインスタンスのIPアドレスにアクセスすると、Nginxのページが表示されました。
その2: M6gインスタンス上でDockerイメージをビルドしてみる
Go言語環境のインストール
今回は、Go言語 (Golang) で記述したプログラムをDockerコンテナで動かしてみます。
YumでGo言語環境をインストールします。
$ sudo yum install golang
インストールされたことを確認します。
$ go version go version go1.13.4 linux/arm64
Go言語プログラムのビルド
ソースコードを格納するディレクトリを作成します。
$ mkdir go-webserver-sample $ cd go-webserver-sample
以下の内容でソースコードを保存します。
package main import ( "fmt" "net/http" "os" "runtime" ) func handler(w http.ResponseWriter, r *http.Request) { hostname, _ := os.Hostname() fmt.Fprintf(w, "<h1>Welcome Golang-WebServer!</h1>") fmt.Fprintf(w, "<h2>Hostname: %s</h2>", hostname) fmt.Fprintf(w, "<h2>OS: %s</h2>", runtime.GOOS) fmt.Fprintf(w, "<h2>Architecture: %s</h2>", runtime.GOARCH) } func main() { http.HandleFunc("/", handler) http.ListenAndServe(":8080", nil) }
ソースコードをビルド (コンパイル) します。
$ go build go-webserver-sample.go
実行可能ファイルとして、拡張子無しのファイルgo-webserver-sample
が作成されます。
$ ls -l total 6832 -rwxrwxr-x 1 ec2-user ec2-user 7049401 May 14 08:14 go-webserver-sample -rw-rw-r-- 1 ec2-user ec2-user 480 May 14 08:00 go-webserver-sample.go
動作確認のために、実行可能ファイルを実行してみましょう。
$ ./go-webserver-sample
TCP/8080ポートで待ち受け状態になります。
Webブラウザで「http://インスタンスのIPアドレス:8080」にアクセスすると、以下のような画面が表示されると思います。
Golangプログラムを含むDockerイメージを作成
さきほどビルドした実行可能ファイルですが、Dockerコンテナで実行させるためにはビルドオプションの追加が必要です。
Go言語は実行可能ファイルをシングルバイナリで生成してくれる言語ですが、通常のビルドオプションではライブラリへのリンク方式が「動的リンク」(実行時リンク) になります。
ビルドを行う環境 (=EC2インスタンス) と実行する環境 (=Dockerコンテナ) が異なる場合、実行時にライブラリへの動的リンクが上手く行えないことがあります。
これを避けるためには、ライブラリへのリンク方式を「静的リンク」(ビルド時リンク) にする必要があります。
静的リンクによるビルドを行うには、以下のようにコマンドを実行します。
$ CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go
- シェル変数
CGO_ENABLED
の値を0
に設定 go build
コマンドのオプション-a
および-installsuffix cgo
を指定
実行可能ファイルを再生成しましたので、Dockerイメージの作成を行いましょう。
以下の内容でDockerfileを保存します。
FROM alpine:latest COPY ./go-webserver-sample /bin/ CMD ["/bin/go-webserver-sample"]
内容としては単純で、軽量Linuxである「Alpine」をベースイメージとして、実行可能ファイルをホストからコンテナへコピーして起動しているだけです。
Docekrイメージのビルドを行います。
$ docker image build -t go-webserver-sample:v1 . Sending build context to Docker daemon 7.053MB Step 1/3 : FROM alpine:latest latest: Pulling from library/alpine 29e5d40040c1: Pull complete Digest: sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c59c978c1ed79109ea4fb9a54 Status: Downloaded newer image for alpine:latest ---> c20d2a9ab686 Step 2/3 : COPY ./go-webserver-sample /bin/ ---> 6ead0d290f19 Step 3/3 : CMD ["/bin/go-webserver-sample"] ---> Running in af0556cc7bd7 Removing intermediate container af0556cc7bd7 ---> 7f57238e2752 Successfully built 7f57238e2752 Successfully tagged go-webserver-sample:v1
ビルドしたDockerイメージを確認しましょう。
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE go-webserver-sample v1 7f57238e2752 5 seconds ago 12.4MB alpine latest c20d2a9ab686 3 weeks ago 5.36MB
コンテナを起動してみます。
$ docker container run -p 80:8080 --rm go-webserver-sample:v1
「TCP/8080」→「TCP/80」へのポートマッピングにより、TCP/80ポートで待ち受け状態になります。
WebブラウザでインスタンスのIPアドレスにアクセスすると、以下のようなページが表示されると思います。
(「Hostname」の表示が、さきほどはEC2インスタンスのホスト名でしたが、今回はコンテナのホスト名となっています)
その3: コンテナ上でGo言語のビルドから実行まで行ってみる
さきほどはホスト上でGo言語のビルドを行いましたが、Dokcerコンテナ上でGo言語のビルドを行うこともできます。
そうすることで、ホスト側にGo言語環境が無くてもソースコードを用意するだけでDockerイメージをビルドすることができます。
Dockerfileの記述
Dockerfileを以下の内容に書き換えます。
FROM golang:latest WORKDIR /tmp COPY ./go-webserver-sample.go /tmp RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go RUN mv /tmp/go-webserver-sample /bin/ CMD ["/bin/go-webserver-sample"]
前節ではベースイメージとして「Alpine」を用いましたが、AlpineにはGo言語をビルドするための環境が含まれていません。
そこで、Go言語環境がインストール済みのコンテナである「golang」をベースイメージとして用いることにします。(1行目)
まず、Go言語のビルドを行います。
- 2行目: 作業用ディレクトリとして「/tmp」を指定します
- 3行目: ホストからコンテナへソースコードファイルをコピーします
- 4行目: Go言語のビルドを実行します
ビルドを行った後は、生成された実行可能ファイルを起動します。
- 5行目: ビルド済み実行可能ファイルを「/tmp」から「/bin」へ移動します
- 6行目: 実行可能ファイルを起動します
Dockerイメージのビルド
それでは、Docekrイメージのビルドを行います。
$ docker image build -t go-webserver-sample:v2 . Sending build context to Docker daemon 3.072kB Step 1/6 : FROM golang:latest latest: Pulling from library/golang d23bf71de5e1: Pull complete d4f6b089b352: Pull complete f34690136adb: Pull complete 4287f76f52e4: Pull complete d5631a7a4659: Pull complete 2a7b5302103f: Pull complete 87401a001075: Pull complete Digest: sha256:b5114a530de5817bcc9b9b5f7b523b0424b75c78dd2f68d2b6d79dc858d98c9f Status: Downloaded newer image for golang:latest ---> 0d65fe43068f Step 2/6 : WORKDIR /tmp ---> Running in b48ac73a2e10 Removing intermediate container b48ac73a2e10 ---> 92d25a8aeaf2 Step 3/6 : COPY ./go-webserver-sample.go /tmp ---> 9165f87ea84c Step 4/6 : RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go ---> Running in ad1b31d928b8 Removing intermediate container ad1b31d928b8 ---> 35587c993f72 Step 5/6 : RUN mv /tmp/go-webserver-sample /bin/ ---> Running in c8b096b1af11 Removing intermediate container c8b096b1af11 ---> af3526e1ccc3 Step 6/6 : CMD ["/bin/go-webserver-sample"] ---> Running in 94d2bb17446a Removing intermediate container 94d2bb17446a ---> 2b8410d3e9e4 Successfully built 2b8410d3e9e4 Successfully tagged go-webserver-sample:v2
ビルドしたDockerイメージを確認しましょう。
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE go-webserver-sample v2 2b8410d3e9e4 About a minute ago 755MB go-webserver-sample v1 7f57238e2752 3 minutes ago 12.4MB golang latest 0d65fe43068f 22 hours ago 714MB alpine latest c20d2a9ab686 3 weeks ago 5.36MB
コンテナを起動してみます。
$ docker container run -p 80:8080 --rm go-webserver-sample:v2
WebブラウザでインスタンスのIPアドレスにアクセスすると、以下のようなページが表示されると思います。
(コンテナのホスト名が異なるのみで、出力結果は前節と変わりませんね)
生成されたDockerイメージの「大きさ」を比べてみると・・・
Dockerコンテナ上でGo言語のビルドを行うことでホスト側にGo言語環境が不要となった訳ですが、ここで、ビルド後のDockerイメージを前節と本節とで比べてみましょう。
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE go-webserver-sample v2 2b8410d3e9e4 About a minute ago 755MB go-webserver-sample v1 7f57238e2752 3 minutes ago 12.4MB golang latest 0d65fe43068f 22 hours ago 714MB alpine latest c20d2a9ab686 3 weeks ago 5.36MB
前節では軽量Linuxイメージ「Alpine」をベースにしたおかげでサイズが「約12MB」と小さくなっていますが、本節ではGo言語環境一式を含む「golang」をベースにしたためサイズが「755MB」とかなり大きくなってしまっています。
イメージのサイズを小さく抑える方法として、次節では「マルチステージビルド」をご紹介します。
その4: マルチステージビルドを使ってみる
「マルチステージビルド」とは?
「マルチステージビルド」とは、一つのDockerfileの中で複数のステップ (ステージ) を記述することで、最終的に生成されるDockerイメージのサイズを抑えることができる仕組みです。
Use multi-stage builds | Docker Documentation
具体的なDockerfileの記述内容を見て行きましょう。
FROM golang:latest AS builder WORKDIR /tmp COPY ./go-webserver-sample.go /tmp RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go FROM alpine:latest COPY --from=builder /tmp/go-webserver-sample /bin/ CMD ["/bin/go-webserver-sample"]
イメージを定義するブロックが2つ書かれています。
1つ目のブロックは「ビルド用コンテナ」すなわち、Go言語のソースコードをビルドして実行可能ファイルを生成するためのコンテナイメージを定義しています。
- 1行目: ベースイメージとして、Go言語環境インストール済みのLinuxイメージである「golang」を指定します (
AS builder
は後からビルド用コンテナの参照を可能にするためのラベルです) - 2行目: 作業用ディレクトリとして「/tmp」を指定します
- 3行目: ホストからコンテナへソースコードファイルをコピーします
- 4行目: Go言語のビルドを実行します
2つ目のブロックは「実行用コンテナ」であり、最終的に出力されるコンテナイメージとなります。
- 6行目: ベースイメージとして「alpine」を指定します。
- 7行目: 「builder」のラベルが付いたコンテナ (=1つ目のブロックで定義されたビルド用コンテナ) からビルド済み実行可能ファイルをコピーします
- 8行目: 実行可能ファイルを起動します
マルチステージビルドによるDockerイメージ作成
では、書き換えたDockerfileを用いてマルチステージビルドを行いましょう。
$ docker image build -t go-webserver-sample:v3 . Sending build context to Docker daemon 3.072kB Step 1/7 : FROM golang:latest AS builder ---> 0d65fe43068f Step 2/7 : WORKDIR /tmp ---> Using cache ---> 92d25a8aeaf2 Step 3/7 : COPY ./go-webserver-sample.go /tmp ---> Using cache ---> 9165f87ea84c Step 4/7 : RUN CGO_ENABLED=0 go build -a -installsuffix cgo go-webserver-sample.go ---> Using cache ---> 35587c993f72 Step 5/7 : FROM alpine:latest ---> c20d2a9ab686 Step 6/7 : COPY --from=builder /tmp/go-webserver-sample /bin/ ---> 36e81eb0d39b Step 7/7 : CMD ["/bin/go-webserver-sample"] ---> Running in ca917a44a9ca Removing intermediate container ca917a44a9ca ---> fe0599c9e44d Successfully built fe0599c9e44d Successfully tagged go-webserver-sample:v3
ビルドしたイメージを確認します。
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE go-webserver-sample v3 fe0599c9e44d 31 seconds ago 12.5MB go-webserver-sample v2 2b8410d3e9e4 3 minutes ago 755MB go-webserver-sample v1 7f57238e2752 5 minutes ago 12.4MB golang latest 0d65fe43068f 22 hours ago 714MB alpine latest c20d2a9ab686 3 weeks ago 5.36MB
最終的に生成されたイメージ「go-webserver-sample:v3」は、前々節で作成した「alpine」ベースのイメージ「go-webserver-sample:v1」とほぼ同じサイズであることが分かります。
最後に、コンテナを起動します。
$ docker container run -p 80:8080 --rm go-webserver-sample:v3
前節、前々節と同様にWebブラウザでアクセスしてみます。
前節と同様に出力結果は変わりません。
おわりに
「Graviton2」上でDockerを使ってコンテナイメージをビルドしたり実行したりするいくつかの方法を試してみました。
次回は「x86」アーキテクチャのDocker環境で「Graviton2」アーキテクチャ用のコンテナイメージをビルドしてみたいと思います。